%==========================================================================
%=  This file is part of the Sound Restoration Project
%=  (c) Copyright Interdisciplinary Mathematics Institute
%=                University of South Carolina, Department of Mathematics
%=  ALL RIGHTS RESERVED
%=
%=  Author: Borislav Karaivanov
%==========================================================================

%==========================================================================
% List of the files on which this procedure depends:
%
% getFileSequenceFromRepresetative.m
% readImageByIndex.m
% readImageFile.m
% readdpx.m
% createMissingDirs.m
% Image processing toolbox
%
%==========================================================================

%==========================================================================
% The function "imagesToVideo" takes a sequence of images, reduces them to
% a number of specified sizes, and for each size writes the reduced images
% INPUT: "inFileName" is a string specifying the name of an image file
% which is part of a whole sequence of image files of the same file format
% residing in the same directory.
% "firstFileInd" and "lastFileInd" are non-negative integers specifying the
% a range of indices outside of which images are not to be part of the
% video.
% "outFileNamePrefix" is a string specifying the common prefix in the names
% of the silent-video output files including full directory path.
% "outFileSuffix" is a string specifying the common suffix in the names of
% the silent-video output files. It is the string after the indexing
% portion of the names including the dot and the file extension.
% "audioDelay" is a real number specifying the delay of the audio with
% respect to the video measured in number of frames. A negative value
% advances the audio.
% "frameRate" is an optional positive integer specifying the desired frame
% rate of the video, i.e., the number of images to be packed in one second 
% of video.
% "desiredHeightCellArr" is an optional cell vector of positive integers or
% strings. Each entry specifies a desired length for the shorter side of
% the images to be used for making video. Positive integers are interpreted
% as length in pixels; the string 'full' specifies that the images in their
% original size are to be used.
% "videoQuality" is an optional integer from 0 through 100 specifying the
% quality of the compressed video. The higher the number, the better the
% video quality.
% "doOverwriteExisting" is an optional boolean variable specifying whether
% or not any existing output files are to be overwritten. By default they
% are overwritten without prompting.
% "waitbarStruct" is an optional parameter specifying a structure of
% parameters used to set a waitbar. Those parameters are a waitbar handle,
% a starting position on the waitbar (a number between 0 and 1), a relative
% length along the waitbar (a number between 0 and (1 - starting position),
% an initial value for a counter that counts the events whose progress is
% being illustrated by the waitbar, and a total reflecting the total number
% of events of the said kind.
% OUTPUT: "outFileNameCellArr" returns a cell array of strings each being
% the name of a produced video file. The number of cells equals the number
% of specified reduction factors.
%==========================================================================
function outFileNameCellArr = imagesToVideo(inFileName, firstFileInd, ...
    lastFileInd, outFileNamePrefix, outFileSuffix, audioDelay, frameRate, ...
    desiredHeightCellArr, videoQuality, doOverwriteExisting, waitbarStruct)
%imagesToVideo('C:\E\NEHScans\NEH044\Test\NEH044_000015.dpx', 15, 31, 'C:\E\NEHScans\NEH044\Test\NEH044_test', '.avi', 0, 24);

if (nargin < 2)
    firstFileInd = 0;
end
if (nargin < 3)
    lastFileInd = inf;
end
if (nargin < 4)
    outFileNamePrefix = '';
end
if (nargin < 5)
    outFileSuffix = '.mp4';
end
if (nargin < 6)
    audioDelay = 0;
end
if (nargin < 7)
    frameRate = 24;
end
if (nargin < 8)
    desiredHeightCellArr = {300; 1080};
end
if (nargin < 9)
    videoQuality = 75;
end
if (nargin < 10)
    doOverwriteExisting = true;
end
% Specify that color images are to be processed as color images.
doConvertToGray = false;

% Assert that the input file exists.
assert((exist(inFileName, 'file') == 2), ...
    'SoundRestoration:imagesToVideo:InFileInexisting', ...
    ['The file %s can not be found.\n' ...
    'A non-empty string specifying a full directory path to an existing file is needed.'], inFileName);


% Find the files residing in the same directory as the given input file and
% having names with the same prefix and extension but a different index.
[inDirPath, inFileNamePrefixNoPath, indFieldLength, inFileExt, fileIndArr] = ...
    getFileSequenceFromRepresetative(inFileName, firstFileInd, lastFileInd);

% Compose a prefix for the names of the output video files.
if (isempty(outFileNamePrefix) == true)
    outFileNamePrefix = [inDirPath inFileNamePrefixNoPath];
end
% Generate the common prefix for the names of the image files.
inFileNamePrefix = [inDirPath inFileNamePrefixNoPath];
% Read in the first image.
[currImageArr, bitDepth] = readImageByIndex(fileIndArr(1), inFileNamePrefix, ...
    indFieldLength, inFileExt, doConvertToGray);
fullSizeImageSizeArr = size(currImageArr);
% Set the color space dimension to 1 for grayscale images, or 3 for color.
colorSpaceDim = 2*numel(fullSizeImageSizeArr) - 3;
% Compute a scaling factor properly converting any integer pixel value to a
% 'uint8' value.
scalingFactor = 2^(8 - bitDepth);

% Allocate storage for the numerical values of the desired sizes.
numSizes = numel(desiredHeightCellArr);
compressedSizesArr = zeros(numSizes, 2);
% Convert the desired sizes to numerical values within the allowed range.
maxHeight = 1088;
maxWidth = 1920;
doFullSize = false;
sizeInd = 1;
for k = 1:numSizes
    if (isnumeric(desiredHeightCellArr{k}) == true)
        
        % Set the compressed height to be equal to the desired height but
        % not greater than both the full-size height and the maximal
        % allowed height.
        compressedSizesArr(sizeInd, :) = [min([desiredHeightCellArr{k}, maxHeight, fullSizeImageSizeArr(1)]), NaN];
        
        % Write a note to the log file explaining why the desired video
        % size is not delivered.
        if (desiredHeightCellArr{k} > maxHeight)
            writeToLogFile(sprintf(['Warning in imagesToVideo: desired image height ' ...
                'of %d exceeds the maximal allowed height of %d'], ...
                desiredHeightCellArr{k}, maxHeight));
            writeToLogFile(sprintf(['Warning in imagesToVideo: to ensure compliance, ' ...
                'image height is reduced to %d'], compressedSizesArr(sizeInd, 1)));
        end
        if (desiredHeightCellArr{k} > fullSizeImageSizeArr(1))
            writeToLogFile(sprintf(['Warning in imagesToVideo: desired image height ' ...
                'of %d exceeds the full-size height of %d'], ...
                desiredHeightCellArr{k}, fullSizeImageSizeArr(1)));
            writeToLogFile(sprintf(['Warning in imagesToVideo: to ensure compliance, ' ...
                'image height is reduced to %d'], compressedSizesArr(sizeInd, 1)));
        end
        
        % Increment the index.
        sizeInd = sizeInd + 1;
        
    elseif (strcmpi(desiredHeightCellArr{k}, 'full') == true)
        
        % Keep note that the last video is to use the full-size images
        % without any compression.
        doFullSize = true;
        
        % Store the current desired height.
        compressedSizesArr(numSizes, :) = fullSizeImageSizeArr(1:2);
        
    end
end

% Allocate memory for the output file names corresponding to the different
% reduced sizes.
outFileNameCellArr = cell(numSizes, 1);
% Generate names for the output files corresponding to the different
% reduced sizes.
doWriteArr = true(numSizes, 1);


% Create a temporary video object allowing to allocate an array for the
% actual video objects.
dummyVideoObj = VideoWriter([outFileNamePrefix 'dummy' outFileSuffix]);
videoObjArr = dummyVideoObj(ones(numSizes, 1));

for k = 1:(numSizes - doFullSize)
    
    % Resize the first image in order to determine its dimensions.
    resizedImageArr = imresize(currImageArr, compressedSizesArr(k, :));
    % Store the dimensions of the resized image.
    compressedSizesArr(k, :) = [size(resizedImageArr, 1), size(resizedImageArr, 2)];
    
    % Make sure that the compressed width is not greater than both the 
    % full-size width and the maximal allowed width. Note that at this
    % point, the compressed width would never be greater than the full-size
    % width.
    if (compressedSizesArr(k, 2) > maxWidth)
        % Resize the first image in order to determine its dimensions.
        resizedImageArr = imresize(currImageArr, [NaN, maxWidth]);
        
        % Write a note to the log file explaining why the desired video
        % size is not delivered.
        writeToLogFile(sprintf(['Warning in imagesToVideo: desired image height ' ...
            'of %d resulted in width of %d, exceeding the maximal allowed width of %d'], ...
            compressedSizesArr(k, 1), compressedSizesArr(k, 2), maxWidth));
        writeToLogFile(sprintf(['Warning in imagesToVideo: to ensure compliance, ' ...
            'image height is reduced to %d'], size(resizedImageArr, 1)));
        
        % Store the dimensions of the resized image.
        compressedSizesArr(k, :) = size(resizedImageArr);
    end
    
    % Make sure that both dimensions of the resized image are even numbers
    % as required by the H.264 video encoder.
    compressedSizesArr(k, :) = compressedSizesArr(k, :) - mod(compressedSizesArr(k, :), 2);
end

for k = 1:numSizes
    % Generate a name for the current output file.
    outFileNameCellArr{k} = [outFileNamePrefix 'Im' num2str(fileIndArr(1)) '-' ...
        num2str(fileIndArr(end)) '_' num2str(compressedSizesArr(k, 1)) 'x' ...
        num2str(compressedSizesArr(k, 2)) '_Delay' num2str(audioDelay) outFileSuffix];
    
    % If the existing video files are not to be overwritten, then check
    % whether a file with the current name exists.
    if ( (doOverwriteExisting == false)&&(exist(outFileNameCellArr{k}, 'file') == 2) )
        warning('SoundRestoration:imagesToVideo:ExistingVideoFile', ...
            ['(in imagesToVideo) the video file %s already exists' ...
            ' and is not to be overwritten.\n'], outFileNameCellArr{k});
        doWriteArr(k) = false;
        continue;
    end
end
% If a full-size output video was requested, make sure an existing copy is
% not overwritten unless so wanted.
if (doFullSize == true)
    doFullSize = doWriteArr(numSizes);
end

% Extract only the sizes for which video files need to be created.
compressedSizesArr = compressedSizesArr(doWriteArr, :);
outFileNameCellArr = outFileNameCellArr(doWriteArr);
numSizes = size(compressedSizesArr, 1);
% If no files are to be written, then return immediately.
if (numSizes == 0)
    return;
end
    
for k = 1:numSizes
    % Create a video object.
    if (ispc)
        if ( (doFullSize == false)||(k < numSizes) )
            videoObjArr(k) = VideoWriter(outFileNameCellArr{k}, 'MPEG-4');
        else
            videoObjArr(k) = VideoWriter(outFileNameCellArr{k}, 'Uncompressed AVI');
        end
    else
        videoObjArr(k) = VideoWriter(outFileNameCellArr{k});
    end
    
    % Set the frame rate.
    videoObjArr(k).FrameRate = frameRate;
    % Set the video quality.
    videoObjArr(k).Quality = videoQuality;
    % Open the video object.
    open(videoObjArr(k));
end

% If the output directory path does not exist, then create any missing
% directory.
createMissingDirs(outFileNameCellArr{1});

% Make sure the delay is an integer.
audioDelay = round(audioDelay);
    
% If the audio starts and finishes before the video, then start the video
% with as many black frames as needed to match the advanced portion of the
% audio.
if (audioDelay < 0)
    % Write out the reduced versions of a black image.
    for k = 1:numSizes
        
        % Create a black image of the same size as the resized images.
        blackImageArr = zeros([compressedSizesArr(k, :), colorSpaceDim, abs(audioDelay)]);
        
        % Append the necessary number of properly resized copies of the
        % black frame to the current video object.
        writeVideo(videoObjArr(k), blackImageArr);
    end
end

% One by one, read in the images and add them to the video object.
for ind = 1:numel(fileIndArr)
    % Read in the current image and the bit depth of its pixels.
   currImageArr = readImageByIndex(fileIndArr(ind), ...
       inFileNamePrefix, indFieldLength, inFileExt, doConvertToGray);
   
   % Write out the reduced versions of the current image.
   for k = 1:numSizes
       % Resize the current image to the desired size.
       resizedImageArr = max(0, min(2^bitDepth, imresize(currImageArr, compressedSizesArr(k, :))));
       % Convert the resized image to 'uint8' since other integer classes
       % are not accepted by the video writer.
       resizedImageArr = uint8(resizedImageArr*scalingFactor);
       % Append the properly resized image to the appropriate video object.
       writeVideo(videoObjArr(k), resizedImageArr);
   end
   
   if (mod(ind, 20) == 0)
       fprintf(1, '%d out of %d images written to video.\n', ind, numel(fileIndArr));
   end
   
    % Update the progress bar.
    if (exist('waitbarStruct', 'var') == 1)
        if getappdata(waitbarStruct.handle, 'canceling')
            return;
        end
        if isempty(waitbarStruct.total)
            waitbarStruct.total = numel(fileIndArr);
        end
        waitbar(waitbarStruct.start + (waitbarStruct.length)*(ind/numel(fileIndArr)), ...
            waitbarStruct.handle, sprintf('Step %d of %d: adding frame %d out of %d to video', ...
            waitbarStruct.index, waitbarStruct.totalIndices, ...
            waitbarStruct.first + ind - 1, waitbarStruct.total));
    end
end

% If the audio starts and finishes after the video, then repeat the last
% frame as many times as needed to match the delayed portion of the audio.
if (audioDelay > 0)

    % Write out the reduced versions of the last image.
    for k = 1:numSizes
        
        % Create a black image of the same size as the resized images.
        blackImageArr = zeros([compressedSizesArr(k, :), colorSpaceDim, abs(audioDelay)]);
        
        % Append the necessary number of properly resized copies of the
        % last frame to the current video object.
        writeVideo(videoObjArr(k), blackImageArr);
    end
end

% Close the existing video objects.
for k = 1:numSizes
    close(videoObjArr(k));
end

return;
% end of the function "imagesToVideo"
